<?php

namespace Gnway\Ticket\Rest;

use Gnway\Ticket\Rest\Config\DispatcherConfig;
use Gnway\Ticket\Rest\Contracts\AbstractFrontRequestDispatcher;
use Gnway\Ticket\Rest\Contracts\HookInterface;
use Gnway\Ticket\Rest\Contracts\HttpResponseInterface;
use Gnway\Ticket\Rest\Facades\Request;
use Gnway\Ticket\Rest\Mapping\AnnotationMappingManager;
use Gnway\Ticket\Rest\Response\ResponseFactory;
use Gnway\Ticket\Rest\Task\TaskExecutor;
use Gnway\Ticket\Rest\Util\FunctionUtils;
use Gnway\Ticket\Rest\Util\MethodUtils;
use Gnway\Ticket\Rest\Util\ResponseUtils;
use Exception, Closure;

defined('API_DISPATCHER') || define('API_DISPATCHER', microtime());

class FrontRequestDispatcher extends AbstractFrontRequestDispatcher
{

  /**
   * Caller class name
   *
   * @var string
   */
  protected string $class;

  /**
   * FrontRequestDispatcher constructor.
   *
   * @param string $class Main class name
   * @param DispatcherConfig $config
   */
  public function __construct(string $class, DispatcherConfig $config)
  {
    $this->class = $class;

    parent::__construct($config);
  }

  /**
   * @inheritDoc
   * @param Closure|null $handler
   */
  public function doDispatch(?Closure $handler = NULL): void
  {
    try {
      $service = Request::get('s') ?? Request::get('service');
      $response = $this->resolveRoute((string)$service)
                       ->doRequest()
                       ->getResponse();

      if (NULL === $handler) {
        ResponseUtils::send($response);

        TaskExecutor::execute();
      } else {
        $this->handleUserDefinedResponse($handler, $response);
      }

      TaskExecutor::clear();

      exit(0);
    } catch (Exception $e) {
      $this->processInternalException($e);
    }
  }

  /**
   * @inheritDoc
   */
  protected function scanPackages(): void
  {
    $packages = $this->config->getPackages();
    $extension = $this->config->getExtension();
    if (empty($packages)) {
      return;
    }

    foreach ($packages as $key => $package) {
      if (!is_numeric($key) && is_dir($key)) {
        [$package, $key] = [$key, $package ?: $extension];
      } else {
        $key = $extension;
      }

      $this->autoloader($package, $key);
    }
  }

  /**
   * @inheritDoc
   * @throws Exception
   */
  protected function scanMappings(): void
  {
    if ($this->config->isUseAnnotation()) {
      (new AnnotationMappingManager)->loadMapping($this->class);
    } else {
      $mapping = $this->config->getMappingFile() ?? '';
      is_file($mapping) && include $mapping;
    }
  }

  /**
   * @inheritDoc
   * @throws Exception
   */
  protected function processBefore()
  {
    return $this->processHandlerHook(HookInterface::BEFORE);
  }

  /**
   * @inheritDoc
   * @throws Exception
   */
  protected function processAfter()
  {
    return $this->processHandlerHook(HookInterface::AFTER);
  }

  /**
   * @inheritDoc
   * @throws Exception
   */
  protected function processReturning($response)
  {
    $result = $this->processHandlerHook(HookInterface::RETURNING, [$response]);
    if ($result !== NULL) {
      return $result;
    }

    return $response;
  }

  /**
   * @inheritDoc
   * @throws Exception
   */
  protected function processException(Exception $ex)
  {
    $result = $this->processHandlerHook(HookInterface::EXCEPTION, [$ex]);
    if ($result !== NULL) {
      return $result;
    }

    throw $ex;
  }

  /**
   * Process handler Hook
   *
   * @param string $signature
   * @param array  $args
   * @return mixed
   * @throws Exception
   */
  protected function processHandlerHook(string $signature, array $args = [])
  {
    if ( ! $this->requestHooks) {
      return NULL;
    }

    foreach ($this->requestHooks as $hook) {
      if ( ! $hook->isAlive($signature)) {
        continue;
      }

      $result = MethodUtils::invoke($hook, $signature, $args);
      if ($result !== NULL) {
        return $result;
      }
    }

    return NULL;
  }

  /**
   * Handle User Defined Response
   *
   * @param Closure                    $handler
   * @param HttpResponseInterface|null $response
   * @throws Exception
   * @return void
   */
  protected function handleUserDefinedResponse(Closure $handler, ?HttpResponseInterface $response): void
  {
    FunctionUtils::invoke($handler, [$response]);
  }

  /**
   * Process handler dispatcher exception
   *
   * @param Exception $ex
   * @return void
   */
  protected function processInternalException(Exception $ex): void
  {
    TaskExecutor::clear();

    $code = (int)$ex->getCode();
    $message = $ex->getMessage();
    $traces = $ex->getTraceAsString();

    if (headers_sent()) {
      trigger_error($message . PHP_EOL . PHP_EOL . $traces, E_USER_ERROR);
    } else {
      if ($this->config->isDebugMode()) {
        $message .= PHP_EOL . PHP_EOL . $traces;
      }
      $message .= PHP_EOL;

      ResponseUtils::send(ResponseFactory::text($message, [], $code > 0 && $code < 600 ? $code : 503), FALSE);
    }

    exit(1);
  }

  /**
   * @inheritDoc
   */
  protected function validateUriChars(string $uri): bool
  {
    $allowedChars = $this->config->getUriAllowedChars();
    if ($allowedChars === '' || $uri === '') {
      return TRUE;
    }

    $pattern = '/^['.$allowedChars.']+$/i';
    $segments = explode('/', $uri);

    foreach ($segments as $segment) {
      if ($segment !== '' && !preg_match($pattern, $segment)) {
        return FALSE;
      }
    }

    return TRUE;
  }
}